From 83d877e9b043e11ee426a050a2f1d9f19b368bdb Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Sun, 21 Sep 2014 17:11:36 -0700 Subject: [PATCH] Implement a package id specification format --- src/cargo/core/mod.rs | 55 ++----- src/cargo/core/package_id.rs | 34 +---- src/cargo/core/package_id_spec.rs | 235 ++++++++++++++++++++++++++++++ src/cargo/util/mod.rs | 2 + src/cargo/util/to_semver.rs | 18 +++ src/cargo/util/toml.rs | 7 +- 6 files changed, 274 insertions(+), 77 deletions(-) create mode 100644 src/cargo/core/package_id_spec.rs create mode 100644 src/cargo/util/to_semver.rs diff --git a/src/cargo/core/mod.rs b/src/cargo/core/mod.rs index 48365629f..50d86caa4 100644 --- a/src/cargo/core/mod.rs +++ b/src/cargo/core/mod.rs @@ -1,48 +1,14 @@ -pub use self::registry::{ - Registry, -}; - -pub use self::manifest::{ - Manifest, - Target, - TargetKind, - Profile -}; - -pub use self::package::{ - Package, - PackageSet -}; - -pub use self::package_id::{ - PackageId -}; - -pub use self::source::{ - Source, - SourceId, - SourceMap, - SourceSet, - GitKind, - PathKind, - RegistryKind -}; - -pub use self::summary::{ - Summary -}; - -pub use self::shell::{ - Shell, - MultiShell, - ShellConfig -}; - -pub use self::dependency::{ - Dependency -}; - +pub use self::dependency::Dependency; +pub use self::manifest::{Manifest, Target, TargetKind, Profile}; +pub use self::package::{Package, PackageSet}; +pub use self::package_id::PackageId; +pub use self::package_id_spec::PackageIdSpec; +pub use self::registry::Registry; pub use self::resolver::Resolve; +pub use self::shell::{Shell, MultiShell, ShellConfig}; +pub use self::source::{PathKind, RegistryKind}; +pub use self::source::{Source, SourceId, SourceMap, SourceSet, GitKind}; +pub use self::summary::Summary; pub mod source; pub mod package; @@ -53,3 +19,4 @@ pub mod resolver; pub mod summary; pub mod shell; pub mod registry; +mod package_id_spec; diff --git a/src/cargo/core/package_id.rs b/src/cargo/core/package_id.rs index 49ee9abc0..810456c74 100644 --- a/src/cargo/core/package_id.rs +++ b/src/cargo/core/package_id.rs @@ -2,34 +2,10 @@ use semver; use std::hash::Hash; use std::fmt::{mod, Show, Formatter}; use std::hash; -use serialize::{ - Encodable, - Encoder, - Decodable, - Decoder -}; - -use util::{CargoResult, CargoError, short_hash}; -use core::source::SourceId; - -trait ToVersion { - fn to_version(self) -> Result; -} - -impl ToVersion for semver::Version { - fn to_version(self) -> Result { - Ok(self) - } -} +use serialize::{Encodable, Encoder, Decodable, Decoder}; -impl<'a> ToVersion for &'a str { - fn to_version(self) -> Result { - match semver::Version::parse(self) { - Ok(v) => Ok(v), - Err(_) => Err(format!("cannot parse '{}' as a semver", self)), - } - } -} +use util::{CargoResult, CargoError, short_hash, ToSemver}; +use core::source::SourceId; #[deriving(Clone, PartialEq, PartialOrd, Ord)] pub struct PackageId { @@ -97,9 +73,9 @@ pub struct Metadata { } impl PackageId { - pub fn new(name: &str, version: T, + pub fn new(name: &str, version: T, sid: &SourceId) -> CargoResult { - let v = try!(version.to_version().map_err(InvalidVersion)); + let v = try!(version.to_semver().map_err(InvalidVersion)); Ok(PackageId { name: name.to_string(), version: v, diff --git a/src/cargo/core/package_id_spec.rs b/src/cargo/core/package_id_spec.rs new file mode 100644 index 000000000..bfce8cd1e --- /dev/null +++ b/src/cargo/core/package_id_spec.rs @@ -0,0 +1,235 @@ +use std::fmt; +use semver::Version; +use url::{mod, Url, UrlParser}; + +use core::PackageId; +use util::{CargoResult, ToUrl, Require, human, ToSemver}; + +#[deriving(Clone, PartialEq, Eq)] +pub struct PackageIdSpec { + name: String, + version: Option, + url: Option, +} + +impl PackageIdSpec { + pub fn parse(spec: &str) -> CargoResult { + if spec.contains("/") { + match spec.to_url() { + Ok(url) => return PackageIdSpec::from_url(url), + Err(..) => {} + } + if !spec.contains("://") { + match url(format!("cargo://{}", spec).as_slice()) { + Ok(url) => return PackageIdSpec::from_url(url), + Err(..) => {} + } + } + } + let mut parts = spec.as_slice().splitn(1, ':'); + let name = parts.next().unwrap(); + let version = match parts.next() { + Some(version) => Some(try!(Version::parse(version).map_err(human))), + None => None, + }; + for ch in name.chars() { + if !ch.is_alphanumeric() && ch != '_' && ch != '-' { + return Err(human(format!("invalid character in pkgid `{}`: `{}`", + spec, ch))) + } + } + Ok(PackageIdSpec { + name: name.to_string(), + version: version, + url: None, + }) + } + + pub fn from_package_id(package_id: &PackageId) -> PackageIdSpec { + PackageIdSpec { + name: package_id.get_name().to_string(), + version: Some(package_id.get_version().clone()), + url: Some(package_id.get_source_id().url.clone()), + } + } + + fn from_url(mut url: Url) -> CargoResult { + if url.query.is_some() { + return Err(human(format!("cannot have a query string in a pkgid: {}", + url))); + } + let frag = url.fragment.take(); + let (name, version) = { + let path = try!(url.path().require(|| { + human(format!("pkgid urls must have a path: {}", url)) + })); + let path_name = try!(path.last().require(|| { + human(format!("pkgid urls must have at least one path \ + component: {}", url)) + })); + match frag { + Some(fragment) => { + let mut parts = fragment.as_slice().splitn(1, ':'); + let name_or_version = parts.next().unwrap(); + match parts.next() { + Some(part) => { + let version = try!(part.to_semver().map_err(human)); + (name_or_version.to_string(), Some(version)) + } + None => { + if name_or_version.char_at(0).is_alphabetic() { + (name_or_version.to_string(), None) + } else { + let version = try!(name_or_version.to_semver() + .map_err(human)); + (path_name.to_string(), Some(version)) + } + } + } + } + None => (path_name.to_string(), None), + } + }; + Ok(PackageIdSpec { + name: name, + version: version, + url: Some(url), + }) + } + + pub fn get_name(&self) -> &str { self.name.as_slice() } + pub fn get_version(&self) -> Option<&Version> { self.version.as_ref() } + pub fn get_url(&self) -> Option<&Url> { self.url.as_ref() } + + pub fn matches(&self, package_id: &PackageId) -> bool { + if self.get_name() != package_id.get_name() { return false } + + match self.version { + Some(ref v) => if v != package_id.get_version() { return false }, + None => {} + } + + match self.url { + Some(ref u) => *u == package_id.get_source_id().url, + None => true + } + } +} + +fn url(s: &str) -> url::ParseResult { + return UrlParser::new().scheme_type_mapper(mapper).parse(s); + + fn mapper(scheme: &str) -> url::SchemeType { + if scheme == "cargo" { + url::RelativeScheme(1) + } else { + url::whatwg_scheme_type_mapper(scheme) + } + } + +} + +impl fmt::Show for PackageIdSpec { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut printed_name = false; + match self.url { + Some(ref url) => { + if url.scheme.as_slice() == "cargo" { + try!(write!(f, "{}/{}", url.host().unwrap(), + url.path().unwrap().connect("/"))); + } else { + try!(write!(f, "{}", url)); + } + if url.path().unwrap().last().unwrap() != &self.name { + printed_name = true; + try!(write!(f, "#{}", self.name)); + } + } + None => { printed_name = true; try!(write!(f, "{}", self.name)) } + } + match self.version { + Some(ref v) => { + try!(write!(f, "{}{}", if printed_name {":"} else {"#"}, v)); + } + None => {} + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use core::{PackageId, SourceId}; + use super::{PackageIdSpec, url}; + use semver::Version; + + #[test] + fn good_parsing() { + fn ok(spec: &str, expected: PackageIdSpec) { + let parsed = PackageIdSpec::parse(spec).unwrap(); + assert_eq!(parsed, expected); + assert_eq!(parsed.to_string().as_slice(), spec); + } + + ok("http://crates.io/foo#1.2.3", PackageIdSpec { + name: "foo".to_string(), + version: Some(Version::parse("1.2.3").unwrap()), + url: Some(url("http://crates.io/foo").unwrap()), + }); + ok("http://crates.io/foo#bar:1.2.3", PackageIdSpec { + name: "bar".to_string(), + version: Some(Version::parse("1.2.3").unwrap()), + url: Some(url("http://crates.io/foo").unwrap()), + }); + ok("crates.io/foo", PackageIdSpec { + name: "foo".to_string(), + version: None, + url: Some(url("cargo://crates.io/foo").unwrap()), + }); + ok("crates.io/foo#1.2.3", PackageIdSpec { + name: "foo".to_string(), + version: Some(Version::parse("1.2.3").unwrap()), + url: Some(url("cargo://crates.io/foo").unwrap()), + }); + ok("crates.io/foo#bar", PackageIdSpec { + name: "bar".to_string(), + version: None, + url: Some(url("cargo://crates.io/foo").unwrap()), + }); + ok("crates.io/foo#bar:1.2.3", PackageIdSpec { + name: "bar".to_string(), + version: Some(Version::parse("1.2.3").unwrap()), + url: Some(url("cargo://crates.io/foo").unwrap()), + }); + ok("foo", PackageIdSpec { + name: "foo".to_string(), + version: None, + url: None, + }); + ok("foo:1.2.3", PackageIdSpec { + name: "foo".to_string(), + version: Some(Version::parse("1.2.3").unwrap()), + url: None, + }); + } + + #[test] + fn bad_parsing() { + assert!(PackageIdSpec::parse("baz:").is_err()); + assert!(PackageIdSpec::parse("baz:1.0").is_err()); + assert!(PackageIdSpec::parse("http://baz:1.0").is_err()); + assert!(PackageIdSpec::parse("http://#baz:1.0").is_err()); + } + + #[test] + fn matching() { + let sid = SourceId::for_central().unwrap(); + let foo = PackageId::new("foo", "1.2.3", &sid).unwrap(); + let bar = PackageId::new("bar", "1.2.3", &sid).unwrap(); + + assert!( PackageIdSpec::parse("foo").unwrap().matches(&foo)); + assert!(!PackageIdSpec::parse("foo").unwrap().matches(&bar)); + assert!( PackageIdSpec::parse("foo:1.2.3").unwrap().matches(&foo)); + assert!(!PackageIdSpec::parse("foo:1.2.2").unwrap().matches(&foo)); + } +} diff --git a/src/cargo/util/mod.rs b/src/cargo/util/mod.rs index 38614d307..6e14c3385 100644 --- a/src/cargo/util/mod.rs +++ b/src/cargo/util/mod.rs @@ -11,6 +11,7 @@ pub use self::dependency_queue::{DependencyQueue, Fresh, Dirty, Freshness}; pub use self::dependency_queue::Dependency; pub use self::graph::Graph; pub use self::to_url::ToUrl; +pub use self::to_semver::ToSemver; pub use self::vcs::{GitRepo, HgRepo}; pub use self::sha256::Sha256; @@ -24,6 +25,7 @@ pub mod paths; pub mod errors; pub mod hex; pub mod profile; +pub mod to_semver; mod pool; mod dependency_queue; mod to_url; diff --git a/src/cargo/util/to_semver.rs b/src/cargo/util/to_semver.rs new file mode 100644 index 000000000..9ea92161d --- /dev/null +++ b/src/cargo/util/to_semver.rs @@ -0,0 +1,18 @@ +use semver::Version; + +pub trait ToSemver { + fn to_semver(self) -> Result; +} + +impl ToSemver for Version { + fn to_semver(self) -> Result { Ok(self) } +} + +impl<'a> ToSemver for &'a str { + fn to_semver(self) -> Result { + match Version::parse(self) { + Ok(v) => Ok(v), + Err(..) => Err(format!("cannot parse '{}' as a semver", self)), + } + } +} diff --git a/src/cargo/util/toml.rs b/src/cargo/util/toml.rs index 0d8deaf52..1e417f549 100644 --- a/src/cargo/util/toml.rs +++ b/src/cargo/util/toml.rs @@ -13,7 +13,7 @@ use core::{SourceId, GitKind}; use core::manifest::{LibKind, Lib, Dylib, Profile}; use core::{Summary, Manifest, Target, Dependency, PackageId}; use core::package_id::Metadata; -use util::{CargoResult, Require, human, ToUrl}; +use util::{CargoResult, Require, human, ToUrl, ToSemver}; /// Representation of the projects file layout. /// @@ -262,10 +262,9 @@ pub struct TomlVersion { impl> Decodable for TomlVersion { fn decode(d: &mut D) -> Result { let s = raw_try!(d.read_str()); - match semver::Version::parse(s.as_slice()) { + match s.as_slice().to_semver() { Ok(s) => Ok(TomlVersion { version: s }), - Err(_) => Err(d.error(format!("cannot parse '{}' as a semver", - s).as_slice())), + Err(e) => Err(d.error(e.as_slice())), } } } -- 2.30.2